﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using LFSRecord2.Events;
using LFSRecord2.Misc;
using LFSRecord2.Model;
using LFSRecord2.Structures;
using LFSRecord2.View.ThreeD;
using LFSRecord2.ViewModel;
using System.IO;
using System.Globalization;
using LFSRecord2.Model.UserActions;

namespace LFSRecord2.View
{
    /// <summary>
    /// Interaction logic for ViewportView.xaml
    /// </summary>
    public partial class ViewportView : UserControl, IDisposable
    {
        #region Properties

        ViewportViews CurrentView = ViewportViews.Top;

        public static DependencyProperty CurrentTrackProperty = DependencyProperty.Register(
          "CurrentTrack",
          typeof(string),
          typeof(ViewportView),
          new FrameworkPropertyMetadata(new PropertyChangedCallback(OnCurrentTrackChanged))
        );
        string _currentTrack = "";
        public string CurrentTrack
        {
            get { return (string)GetValue(CurrentTrackProperty); }
            set { SetValue(CurrentTrackProperty, value); }
        }
        private static void OnCurrentTrackChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ViewportView)d)._currentTrack = ((ViewportView)d).CurrentTrack;
            ((ViewportView)d).SetNewTrack();
        }

        public static DependencyProperty WorldCamProperty = DependencyProperty.Register(
          "WorldCam",
          typeof(LfsCamera),
          typeof(ViewportView),
          new FrameworkPropertyMetadata(new LfsCamera(), new PropertyChangedCallback(OnWorldCamChanged))
        );
        public LfsCamera WorldCam
        {
            get { return (LfsCamera)GetValue(WorldCamProperty); }
            set { SetValue(WorldCamProperty, value); }
        }
        private static void OnWorldCamChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            //Debug.WriteLine("Viewport OnWorldCamChanged");
            if (((ViewportView)d).CurrentView == ViewportViews.ThreeD)
                ((ViewportView)d).apply3DTransform();
        }

        BackgroundWorker _trackMeshImportWorker = new BackgroundWorker();

        #endregion Properties

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                _trackMeshImportWorker.DoWork -= bgWorkerTrackMesh_DoWork;
                _trackMeshImportWorker.RunWorkerCompleted -= bgWorkerTrackMesh_RunWorkerCompleted;
                _trackMeshImportWorker.Dispose();
            }
        }

        public ViewportView()
        {
            InitializeComponent();

            _trackMeshImportWorker.DoWork += bgWorkerTrackMesh_DoWork;
            _trackMeshImportWorker.RunWorkerCompleted += bgWorkerTrackMesh_RunWorkerCompleted;
            //_trackMeshImportWorker.ProgressChanged += new ProgressChangedEventHandler(bgWorkerTrackMesh_ProgressChanged);
        }

        private void bgWorkerTrackMesh_DoWork(object sender, DoWorkEventArgs e)
        {
            LfsTrackMesh mesh = new LfsTrackMesh(LfsRecordController.StartupDirectory + "\\data\\smx\\" + _currentTrack + "_3DH.smx");

            if (mesh.Load())
                e.Result = mesh;
            else
                e.Cancel = true;
        }

        private void bgWorkerTrackMesh_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            // First, handle the case where an exception was thrown.
            if (e.Error != null)
            {
                MessageBox.Show(e.Error.Message);
            }
            else if (e.Cancelled)
            {
                MessageBox.Show("Problem loading track mesh '" + _currentTrack + "_3DH.smx'. File missing?");
            }
            else
            {
                Create3DWorld((LfsTrackMesh)e.Result);
            }
        }

        private void ViewportView_Loaded(object sender, RoutedEventArgs e)
        {
            // Bind some values
            Binding myBinding = new Binding("CurrentTrack");
            myBinding.Mode = BindingMode.OneWay;
            myBinding.Source = (ViewportViewModel)DataContext;
            this.SetBinding(ViewportView.CurrentTrackProperty, myBinding);

            myBinding = new Binding("WorldCam");
            myBinding.Mode = BindingMode.OneWay;
            myBinding.Source = (ViewportViewModel)DataContext;
            this.SetBinding(ViewportView.WorldCamProperty, myBinding);

            // Layer listener (3d layer (null) objects)
            Layer3DObject layerObject; 
            foreach (CompLayerBase layer in ((ViewportViewModel)DataContext).AllLayers)
            {
                layerObject = new Layer3DObject(layer);
                _mvLayerObjects.Add(layerObject);
                //Debug.WriteLine("added an _mvNull (" + _mvNulls.Count + ")");
                if (layer.LayerType == CompLayerTypes.Null || layer.LayerType == CompLayerTypes.ReplayData)
                {
                    vp3D.Children.Add(layerObject.LayerModelView);
                }
            }
            ((ViewportViewModel)DataContext).AllLayers.CollectionChanged += AllLayers_CollectionChanged;

            // Setup combobox items
            UpdateViewportScaleComboBox();
            UpdateViewportViewsComboBox();

            // Setup 2D / 3D views
            Setup3DView();
            Setup2DView();
        }

        private void AllLayers_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            //Debug.WriteLine("AllLayers collection changed : " + e.Action + " - " + e.NewStartingIndex + " - " + e.OldStartingIndex);

            // Here we check when to create or remove 3d null objects
            int index;
            Layer3DObject layerObject;
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Reset:
                    for (int a = 0; a < _mvLayerObjects.Count; a++)
                    {
                        layerObject = _mvLayerObjects[a];
                        vp3D.Children.Remove(layerObject.LayerModelView);
                    }
                    _mvLayerObjects.Clear();
                    break;

                case NotifyCollectionChangedAction.Add:
                    foreach (CompLayerBase layer in e.NewItems)
                    {
                        index = _mvLayerObjects.Count;
                        layerObject = new Layer3DObject(layer);
                        _mvLayerObjects.Add(layerObject);
                        //Debug.WriteLine("added an _mvNull (" + _mvNulls.Count + ")");
                        if (layer.LayerType == CompLayerTypes.Null || layer.LayerType == CompLayerTypes.ReplayData)
                        {
                            vp3D.Children.Add(layerObject.LayerModelView);
                        }
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:
                    layerObject = _mvLayerObjects[e.OldStartingIndex];
                    _mvLayerObjects.RemoveAt(e.OldStartingIndex);

                    if (((CompLayerBase)e.OldItems[0]).LayerType == CompLayerTypes.Null || ((CompLayerBase)e.OldItems[0]).LayerType == CompLayerTypes.ReplayData)
                        vp3D.Children.Remove(layerObject.LayerModelView);
                    //Debug.WriteLine("removed an _mvNull (" + _mvNulls.Count + ")");
                    break;

                default:
                    Debug.WriteLine("UNHANDLED VIEWPORT ALLLAYERS COLLECTION CHANGED");
                    break;
            }

            UpdateViewportViewsComboBox();
        }

        private void UpdateViewportScaleComboBox()
        {
            ScaleSelectionBox.Items.Clear();

            ScaleSelectionBox.Items.Add(Math.Round(MapScale * 100) + "%");
            ScaleSelectionBox.Items.Add(new Separator());
            ScaleSelectionBox.Items.Add("25%");
            ScaleSelectionBox.Items.Add("50%");
            ScaleSelectionBox.Items.Add("100%");
            ScaleSelectionBox.Items.Add("200%");
            ScaleSelectionBox.Items.Add("400%");
            ScaleSelectionBox.Items.Add("800%");

            ScaleSelectionBox.SelectedIndex = 0;
        }

        private void UpdateViewportViewsComboBox()
        {
            List<string> cameraItems = new List<string>();
            List<object> newItems = new List<object>();

            foreach (CompLayerBase layer in ((ViewportViewModel)DataContext).AllLayers)
            {
                if (layer.LayerType == CompLayerTypes.LookCamera ||
                    layer.LayerType == CompLayerTypes.FreeCamera)
                    cameraItems.Add(layer.LayerName);
            }

            if (cameraItems.Count > 0)
            {
                newItems.Add("Active Camera");
                //newItems.Add(new Separator());
                //foreach (object item in cameraItems)
                //    newItems.Add(item);
                newItems.Add(new Separator());
            }
            else
            {
                if (CurrentView == ViewportViews.ThreeD)
                {
                    SetNewViewType("Top");
                }
            }

            newItems.Add("Top");
            newItems.Add("Front");
            newItems.Add("Left");
            newItems.Add("Bottom");
            newItems.Add("Back");
            newItems.Add("Right");

            ViewSelectionBox.ItemsSource = newItems;

            switch (CurrentView)
            {
                case ViewportViews.ThreeD:
                    ViewSelectionBox.Text = "Active Camera";
                    break;
                default:
                    ViewSelectionBox.Text = CurrentView.ToString();
                    break;
            }
        }

        // Raw track visual-data
        //LFSTrackMesh _trackMesh;
        ImageDrawing _trackBitmapImage = null;
        ImageDrawing _horizonBackGroundImage = null;

        public void SetNewTrack()
        {
            Debug.WriteLine("New selected track : '" + CurrentTrack + "'");

            if (CurrentTrack.Length == 0)
                return;
            
            // Load 3D visual-data
            _trackMeshImportWorker.RunWorkerAsync();

            // Load 2D visual-data
            string path = LfsRecordController.StartupDirectory + "\\data\\bitmaps\\tracks\\" + CurrentTrack.Replace(" ", "_") + ".png";
            if (File.Exists(path))
            {
                _trackBitmapImage.ImageSource = new BitmapImage(new Uri(path, UriKind.Absolute));
            }
            else
            {
                MessageBox.Show(Application.Current.MainWindow, "Problem loading track bitmap : File does not exist (" + path + ")", "Track load error", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            _trackBitmapImage.Rect = new Rect(0, 0, 2560, 2560);
        }

        public void SetTrackBackground(ViewportViews newView)
        {
            switch (newView)
            {
                case ViewportViews.Bottom:
                    image2DDrawings.Children.Clear();
                    image2DDrawings.Children.Add(_trackBitmapImage);
                    Map2DImage.RenderTransform = new ScaleTransform(1, -1);
                    break;
                case ViewportViews.Front:
                case ViewportViews.Back:
                case ViewportViews.Left:
                case ViewportViews.Right:
                    image2DDrawings.Children.Clear();
                    image2DDrawings.Children.Add(_horizonBackGroundImage);
                    Map2DImage.RenderTransform = new ScaleTransform(1, 1);
                    break;
                default:
                    image2DDrawings.Children.Clear();
                    image2DDrawings.Children.Add(_trackBitmapImage);
                    Map2DImage.RenderTransform = new ScaleTransform(1, 1);
                    break;
            }
        }

        #region 3DWorld

        // Model view and group for Track
        ModelVisual3D _mvTrack = new ModelVisual3D();
        Model3DGroup _mgTrack = new Model3DGroup();

        List<Layer3DObject> _mvLayerObjects = new List<Layer3DObject>();

        // Persistent world objects
        PerspectiveCamera _cam3D = new PerspectiveCamera();
        DirectionalLight _sunLight;
        AmbientLight _ambientLight;

        private void Setup3DView()
        {
            WorldCam.PosX = 0;
            WorldCam.PosY = 0;
            WorldCam.PosZ = 20;
            WorldCam.Heading = 0;
            WorldCam.Pitch = 0;
            WorldCam.Roll = 0;
            WorldCam.FOV = 60;

            // Create 3d camera
            _cam3D.Position = new Point3D(0, 0, 0);
            _cam3D.LookDirection = new Vector3D(0, -1, 0);
            _cam3D.UpDirection = new Vector3D(0, 0, 1);
            _cam3D.FieldOfView = WorldCam.FOV;

            // Create lights
            _sunLight = new DirectionalLight(Colors.White, new Vector3D(-0.5, -0.5, -0.5));
            _ambientLight = new AmbientLight(Colors.SlateGray);

            // ModelVisual3D
            vp3D.Camera = _cam3D;
            vp3D.Children.Add(_mvTrack);

            vp3D.Visibility = (CurrentView != ViewportViews.ThreeD) ? Visibility.Hidden : Visibility.Visible;
        }

        //void Create3DWorld()
        void Create3DWorld(LfsTrackMesh trackMesh)
        {
            // Clear world
            _mvTrack.Content = null;
            _mgTrack = new Model3DGroup();

            // Add the sun (and ambient)
            _mgTrack.Children.Add(_sunLight);
            _mgTrack.Children.Add(_ambientLight);

            // Add the track mesh objects
            MeshGeometry3D mGeom;
            SolidColorBrush objectBrush;
            DiffuseMaterial objectMaterial;
            //Debug.WriteLine("num objects in mesh : {0}", trackMesh.NumObjects);
            for (int a = 0; a < trackMesh.NumObjects; a++)
            {
                mGeom = new MeshGeometry3D();
                mGeom.Positions = new Point3DCollection(trackMesh.TrackMeshObjects[a].Points);
                mGeom.TriangleIndices = new Int32Collection(trackMesh.TrackMeshObjects[a].Tris);
                mGeom.Freeze();

                objectBrush = new SolidColorBrush(trackMesh.TrackMeshObjects[a].ObjectColor);
                objectBrush.Freeze();
                objectMaterial = new DiffuseMaterial(objectBrush);
                objectMaterial.Freeze();
                _mgTrack.Children.Add(new GeometryModel3D(mGeom, objectMaterial));
            }

            if (CurrentView != ViewportViews.ThreeD)
                vp3D.Visibility = Visibility.Hidden;
            //_mgTrack.Freeze();
            _mvTrack.Content = _mgTrack;
            
            apply3DTransform();
        }

        private void apply3DTransform()
        {
            //Debug.WriteLine("3d transform - Z : " + WorldCam.Z);
            
            _cam3D.FieldOfView = WorldCam.FOV;
            //_mgTrack.Transform = new MatrixTransform3D(WorldCam.mTr);

            Transform3DGroup trGroup = new Transform3DGroup();
            trGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 2), WorldCam.Heading), WorldCam.PosX, -WorldCam.PosY, WorldCam.PosZ));
            trGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(2, 0, 0), -WorldCam.Pitch), WorldCam.PosX, -WorldCam.PosY, WorldCam.PosZ));
            trGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 2, 0), WorldCam.Roll), WorldCam.PosX, -WorldCam.PosY, WorldCam.PosZ));
            trGroup.Children.Add(new TranslateTransform3D(-WorldCam.PosX, WorldCam.PosY, -WorldCam.PosZ));

            _mgTrack.Transform = trGroup;

            for (int a = 0; a < _mvLayerObjects.Count; a++)
            {
                _mvLayerObjects[a].LayerModelView.Transform = trGroup;
            }
        }

        #endregion 3DWorld

        #region 2DWorld

        DrawingGroup image2DDrawings = new DrawingGroup();      // Contains all the Drawing objects (trackmap, lines, rectangles, etc)
        //bool drawQuickPath = false;                             // If true, the frame rectangles won't be drawn while adjusting the path
        Point MapCoord = new Point(0, 0);                       // Current map coordinates (for moving the 2d contents around)
        Point MapAnchor = new Point(1280, 1280);                // Anchor point of the 2d map
        double MapScale = 1.0d;

        private void Setup2DView()
        {
            // Create _horizonBackGroundImage
            GradientStopCollection gsColl = new GradientStopCollection();
            gsColl.Add(new GradientStop(Color.FromArgb(255, 140, 140, 140), 0.0));
            gsColl.Add(new GradientStop(Color.FromArgb(255, 140, 140, 140), 0.4999));
            gsColl.Add(new GradientStop(Color.FromArgb(255, 110, 110, 110), 0.5001));
            gsColl.Add(new GradientStop(Color.FromArgb(255, 110, 110, 110), 1.0));
            LinearGradientBrush gBrush = new LinearGradientBrush();
            gBrush.StartPoint = new Point(0.5, 0.0);
            gBrush.EndPoint = new Point(0.5, 1.0);
            gBrush.GradientStops = gsColl;

            GeometryDrawing blankGeomertyDrawing = new GeometryDrawing();
            blankGeomertyDrawing.Geometry = new RectangleGeometry(new Rect(0, 0, 2560, 2560));
            blankGeomertyDrawing.Brush = gBrush;
            DrawingImage blankGeometryImage = new DrawingImage(blankGeomertyDrawing);
            blankGeometryImage.Freeze();

            _horizonBackGroundImage = new ImageDrawing();
            _horizonBackGroundImage.ImageSource = blankGeometryImage;
            _horizonBackGroundImage.Rect = new Rect(0, 0, 2560, 2560);

            // Blank (transparent) Map2DImage to start with
            blankGeomertyDrawing = new GeometryDrawing();
            blankGeomertyDrawing.Geometry = new RectangleGeometry(new Rect(0, 0, 2560, 2560));
            blankGeomertyDrawing.Brush = Brushes.Transparent;
            blankGeometryImage = new DrawingImage(blankGeomertyDrawing);
            blankGeometryImage.Freeze();

            _trackBitmapImage = new ImageDrawing();
            _trackBitmapImage.ImageSource = blankGeometryImage;
            _trackBitmapImage.Rect = new Rect(0, 0, 2560, 2560);

            Create2DWorld();

            if (CurrentView == ViewportViews.ThreeD)
                tab2DCanvas.Visibility = Visibility.Hidden;
            else
                tab2DCanvas.Visibility = Visibility.Visible;
        }

        private void Create2DWorld()
        {
            Map2DImage.Source = new DrawingImage(image2DDrawings);
            Map2DImage.RenderTransformOrigin = new Point(0.5, 0.5);
            SetTrackBackground(CurrentView);

            apply2DTransform();
        }

        #endregion 2DWorld

        #region GenericTransform

        OrigCamState _origCam;
        private void viewportCanvas_MouseDown(object sender, MouseButtonEventArgs e)
        {
            CompLayerPropertyPoint3D propPos, propLookAt;
            CompLayerPropertyDouble propHeading, propPitch;

            Point mPos = e.GetPosition(this);
            //Debug.WriteLine("Clicked at " + mPos.X + ", " + mPos.Y);

            // Record original state of the camera
            if (LfsRecordController.project.GetActiveCameraType() == CameraTypes.LookCamera)
            {
                CompLayerLookCamera cl = (CompLayerLookCamera)LfsRecordController.project.GetActiveCameraLayer();
                propPos = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerLookCamera.PropertyTypes.Position];
                propLookAt = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerLookCamera.PropertyTypes.LookAt];
                _origCam = new OrigCamState()
                {
                    Position = new Point3D(propPos.CurrentXValue, propPos.CurrentYValue, propPos.CurrentZValue),
                    LookAt = new Point3D(propLookAt.CurrentXValue, propLookAt.CurrentYValue, propLookAt.CurrentZValue),
                };
            }
            else if (LfsRecordController.project.GetActiveCameraType() == CameraTypes.FreeCamera)
            {
                CompLayerFreeCamera cl = (CompLayerFreeCamera)LfsRecordController.project.GetActiveCameraLayer();
                propPos = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Position];
                propHeading = (CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Heading];
                propPitch = (CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Pitch];
                _origCam = new OrigCamState
                {
                    Position = new Point3D(propPos.CurrentXValue, propPos.CurrentYValue, propPos.CurrentZValue),
                    Heading = propHeading.CurrentValue,
                    Pitch = propPitch.CurrentValue
                };
            }

            // Start mouse move capturing
            Application.Current.MainWindow.CaptureMouse();
            Application.Current.MainWindow.MouseMove += viewportCanvas_MouseMove;
            Application.Current.MainWindow.MouseUp += viewportCanvas_MouseUp;
            _prevPoint = e.GetPosition(this);

        }

        private void viewportCanvas_MouseUp(object sender, MouseButtonEventArgs e)
        {
            CompLayerPropertyPoint3D propPos, propLookAt;
            CompLayerPropertyDouble propHeading, propPitch;
            CustomPoint3DKeyFrame kfp;
            CustomDoubleKeyFrame kfd;
            Point3D pos, lookat;
            int keyframeIndex;

            // End mouse move capturing
            Application.Current.MainWindow.ReleaseMouseCapture();
            Application.Current.MainWindow.MouseMove -= viewportCanvas_MouseMove;
            Application.Current.MainWindow.MouseUp -= viewportCanvas_MouseUp;

            // Record the user action (take care - anything from position, lookat, heading and pitch can be changed here
            // First figure out what type of camera we were adjusting
            if (LfsRecordController.project.GetActiveCameraType() == CameraTypes.LookCamera)
            {
                CompLayerLookCamera cl = (CompLayerLookCamera)LfsRecordController.project.GetActiveCameraLayer();

                // Check if position has changed
                propPos = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerLookCamera.PropertyTypes.Position];
                pos = new Point3D(propPos.CurrentXValue, propPos.CurrentYValue, propPos.CurrentZValue);
                if (_origCam.Position != pos)
                {
                    // Keyframe or current value?
                    kfp = (CustomPoint3DKeyFrame)propPos.GetKeyAtTime(LfsRecordController.FrameIndex);
                    keyframeIndex = kfp == null ? -1 : kfp.KeyFrameIndex;
                    LfsRecordController.RecordUserAction(new UserActionKeyFramePoint3DValueChanged()
                        {
                            LayerIndex = cl.LayerIndex,
                            PropertyIndex = propPos.PropertyIndex,
                            KeyFrameIndex = keyframeIndex,
                            ValueOffset = Point3D.Subtract(pos, _origCam.Position)
                        });
                }

                // Check if lookat has changed
                propLookAt = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerLookCamera.PropertyTypes.LookAt];
                lookat = new Point3D(propLookAt.CurrentXValue, propLookAt.CurrentYValue, propLookAt.CurrentZValue);
                if (_origCam.LookAt != lookat)
                {
                    // Keyframe or current value?
                    kfp = (CustomPoint3DKeyFrame)propLookAt.GetKeyAtTime(LfsRecordController.FrameIndex);
                    keyframeIndex = kfp == null ? -1 : kfp.KeyFrameIndex;
                    LfsRecordController.RecordUserAction(new UserActionKeyFramePoint3DValueChanged()
                    {
                        LayerIndex = cl.LayerIndex,
                        PropertyIndex = propLookAt.PropertyIndex,
                        KeyFrameIndex = keyframeIndex,
                        ValueOffset = Point3D.Subtract(lookat, _origCam.LookAt)
                    });
                }
            }
            else if (LfsRecordController.project.GetActiveCameraType() == CameraTypes.FreeCamera)
            {
                CompLayerFreeCamera cl = (CompLayerFreeCamera)LfsRecordController.project.GetActiveCameraLayer();
                propPos = (CompLayerPropertyPoint3D)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Position];
                propHeading = (CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Heading];
                propPitch = (CompLayerPropertyDouble)cl.Properties[(int)CompLayerFreeCamera.PropertyTypes.Pitch];

                // Check if position has changed
                pos = new Point3D(propPos.CurrentXValue, propPos.CurrentYValue, propPos.CurrentZValue);
                if (_origCam.Position != pos)
                {
                    // Keyframe or current value?
                    kfp = (CustomPoint3DKeyFrame)propPos.GetKeyAtTime(LfsRecordController.FrameIndex);
                    keyframeIndex = kfp == null ? -1 : kfp.KeyFrameIndex;

                    LfsRecordController.RecordUserAction(new UserActionKeyFramePoint3DValueChanged()
                    {
                        LayerIndex = cl.LayerIndex,
                        PropertyIndex = propPos.PropertyIndex,
                        KeyFrameIndex = keyframeIndex,
                        ValueOffset = Point3D.Subtract(pos, _origCam.Position)
                    });
                }

                // Check if heading has changed
                if (_origCam.Heading != propHeading.CurrentValue)
                {
                    // Keyframe or current value?
                    kfd = (CustomDoubleKeyFrame)propHeading.GetKeyAtTime(LfsRecordController.FrameIndex);
                    keyframeIndex = kfd == null ? -1 : kfd.KeyFrameIndex;

                    LfsRecordController.RecordUserAction(new UserActionKeyFrameDoubleValueChanged()
                    {
                        LayerIndex = cl.LayerIndex,
                        PropertyIndex = propHeading.PropertyIndex,
                        KeyFrameIndex = keyframeIndex,
                        ValueOffset = propHeading.CurrentValue - _origCam.Heading
                    });
                }

                // Check if pitch has changed
                if (_origCam.Pitch != propPitch.CurrentValue)
                {
                    // Keyframe or current value?
                    kfd = (CustomDoubleKeyFrame)propPitch.GetKeyAtTime(LfsRecordController.FrameIndex);
                    keyframeIndex = kfd == null ? -1 : kfd.KeyFrameIndex;

                    LfsRecordController.RecordUserAction(new UserActionKeyFrameDoubleValueChanged()
                    {
                        LayerIndex = cl.LayerIndex,
                        PropertyIndex = propPitch.PropertyIndex,
                        KeyFrameIndex = keyframeIndex,
                        ValueOffset = propPitch.CurrentValue - _origCam.Pitch
                    });
                }
            }

            LfsRecordController.CreateUndoLevel();
        }

        Point _prevPoint;
        void viewportCanvas_MouseMove(object sender, MouseEventArgs e)
        {
            Point mPos = e.GetPosition(this);
            Point diff;
            if (CurrentView == ViewportViews.ThreeD && LfsRecordController.project.Move3DAxesInvert)
                diff = new Point(mPos.X - _prevPoint.X, mPos.Y - _prevPoint.Y);
            else
                diff = new Point(_prevPoint.X - mPos.X, _prevPoint.Y - mPos.Y);
            _prevPoint = mPos;

            if (CurrentView == ViewportViews.ThreeD)
            {
                // Create new camera object
                LfsCamera newCam = LfsRecordController.project.GetActiveCameraAtIndex(LfsRecordController.FrameIndex);
                if (newCam == null)
                    return;

                // 3d view transform values
                if (e.LeftButton == MouseButtonState.Pressed)
                {
                    if (newCam.CameraType == CameraTypes.FreeCamera)
                    {
                        double angle = WorldCam.Heading * Math.PI / 180;
                        double xNew = Math.Cos(angle) * diff.X - Math.Sin(angle) * diff.Y;
                        double yNew = Math.Sin(angle) * diff.X + Math.Cos(angle) * diff.Y;

                        newCam.PosX -= (double)(xNew / 10.0d);
                        newCam.PosY -= (double)(yNew / 10.0d);
                    }
                    else if (newCam.CameraType == CameraTypes.LookCamera)
                    {
                        if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
                        {
                            // Adjust distance to lookat (diff.Y)
                            Vector3D lookVec = Point3D.Subtract(newCam.LookAt, new Point3D(newCam.PosX, newCam.PosY, newCam.PosZ));
                            //Debug.WriteLine("Length : {0} + {1}", lookVec.Length, diff.Y);
                            if (lookVec.Length - diff.Y > 0.1d)
                            {
                                lookVec.Normalize();
                                newCam.PosX += lookVec.X * diff.Y;
                                newCam.PosY += lookVec.Y * diff.Y;
                                newCam.PosZ += lookVec.Z * diff.Y;
                            }

                            // Adjust rotation around lookat (diff.X)
                            Point3D newPos = MathHelper.RotateZ(
                                diff.X / 500.0d,
                                new Point3D(
                                    newCam.PosX - newCam.LookAt.X,
                                    newCam.PosY - newCam.LookAt.Y,
                                    newCam.PosZ - newCam.LookAt.Z));
                            newCam.PosX = newCam.LookAt.X + newPos.X;
                            newCam.PosY = newCam.LookAt.Y + newPos.Y;
                            newCam.PosZ = newCam.LookAt.Z + newPos.Z;

                            // Set heading and pitch accordingly
                            lookVec = Point3D.Subtract(newCam.LookAt, new Point3D(newCam.PosX, newCam.PosY, newCam.PosZ));
                            //Debug.WriteLine("NEW Length : {0}", lookVec.Length);
                            lookVec.Normalize();
                            newCam.Heading = Math.Atan2(lookVec.Y, lookVec.X) * MathHelper.RadDegMult - 90;
                            newCam.Pitch = -Math.Atan2(
                                lookVec.Z, Math.Sqrt(lookVec.X * lookVec.X + lookVec.Y * lookVec.Y)
                                ) * MathHelper.RadDegMult;
                        }
                        else
                        {
                            // Plainly move both position and lookat by same x/y coord
                            double angle = WorldCam.Heading * Math.PI / 180;
                            double xNew = Math.Cos(angle) * diff.X - Math.Sin(angle) * diff.Y;
                            double yNew = Math.Sin(angle) * diff.X + Math.Cos(angle) * diff.Y;

                            newCam.PosX -= (double)(xNew / 10);
                            newCam.PosY -= (double)(yNew / 10);
                            newCam.LookAt = new Point3D(
                                newCam.LookAt.X - (double)(xNew / 10),
                                newCam.LookAt.Y - (double)(yNew / 10),
                                newCam.LookAt.Z
                                );
                        }
                    }
                }
                else if (e.RightButton == MouseButtonState.Pressed)
                {
                    // Rotate around our own center point
                    newCam.Heading += (double)(diff.X / 10);
                    //newCam.Heading = newCam.Heading % 360;
                    newCam.Pitch += (double)(diff.Y / 10);
                    //newCam.Pitch = newCam.Pitch % 360;
                    
                    if (newCam.CameraType == CameraTypes.LookCamera)
                    {
                        //Debug.WriteLine("Rotating look at point");
                        // Create new lookat point
                        Vector3D lookVec = Point3D.Subtract(newCam.LookAt, new Point3D(newCam.PosX, newCam.PosY, newCam.PosZ));
                        Point3D origLookAt = new Point3D(0, lookVec.Length, 0);
                        newCam.LookAt = MathHelper.Point3DFromHeadingPitch(origLookAt, newCam.Heading, -newCam.Pitch);
                        newCam.LookAt = new Point3D(
                            newCam.LookAt.X + newCam.PosX,
                            newCam.LookAt.Y + newCam.PosY,
                            newCam.LookAt.Z + newCam.PosZ
                            );
                    }
                }

                //WorldCam = newCam;
                ((ViewportViewModel)DataContext).WorldCam = newCam;
                //apply3DTransform();
            }
            else
            {
                if (e.LeftButton == MouseButtonState.Pressed)
                {
                    // 2d view transform
                    MapCoord.X -= diff.X;
                    MapCoord.Y -= diff.Y;
                    apply2DTransform();
                }
            }
        }

        private void viewportCanvas_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (CurrentView == ViewportViews.ThreeD || (MapScale <= 0.25 && e.Delta < 0))
                return;

            Point clickOriginOnMap = this.TranslatePoint(e.GetPosition(this), Map2DImage);
            clickOriginOnMap.X -= 1280;
            clickOriginOnMap.Y -= 1280;
            //Debug.WriteLine("Map coordinates (where mouse pointer is) : " + clickOriginOnMap.X + ", " + clickOriginOnMap.Y);

            double oldScale = MapScale;
            MapScale += (e.Delta / 480.0f);
            if (MapScale < 0.25f)
                MapScale = 0.25f;

            // Map placement correction after zoom
            _adjustMapCoordAfterScale(oldScale, clickOriginOnMap);

            apply2DTransform();

            UpdateViewportScaleComboBox();

            ServicesFactory.EventService.GetEvent<ViewportScaleChangedEvent>().Publish(MapScale);
        }

        private void ScaleSelectionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            e.Handled = true;
            if (e.AddedItems.Count < 1)
                return;
            string item = (string)e.AddedItems[0];
            if (ScaleSelectionBox.Items.Count > 0 && item == (string)ScaleSelectionBox.Items[0])
                return;

            int percent = int.Parse(item.Substring(0, item.Length - 1), CultureInfo.InvariantCulture);

            double oldScale = MapScale;
            MapScale = percent / 100.0d;

            // Map placement correction after zoom
            Point clickOriginOnMap = this.TranslatePoint(new Point(Math.Round(tab3DCanvas.ActualWidth / 2), Math.Round(tab3DCanvas.ActualHeight / 2)), Map2DImage);
            clickOriginOnMap.X -= 1280;
            clickOriginOnMap.Y -= 1280;
            _adjustMapCoordAfterScale(oldScale, clickOriginOnMap);

            apply2DTransform();

            UpdateViewportScaleComboBox();

            ServicesFactory.EventService.GetEvent<ViewportScaleChangedEvent>().Publish(MapScale);
        }

        private void _adjustMapCoordAfterScale(double oldScale, Point clickOrigin)
        {
            double scaleMult = oldScale / MapScale;
            double offsetX = clickOrigin.X - (clickOrigin.X * scaleMult);
            double offsetY = clickOrigin.Y - (clickOrigin.Y * scaleMult);
            MapCoord.X -= offsetX * MapScale;
            MapCoord.Y -= offsetY * MapScale;
        }

        #endregion GenericTransform

        #region 2DWorldTransform

        void apply2DTransform()
        {
            //Debug.WriteLine("Apply2DTransform");
            TransformGroup trGroup = new TransformGroup();
            trGroup.Children.Add(new TranslateTransform(
                (MapCoord.X - MapAnchor.X) / MapScale + tab3DCanvas.ActualWidth / 2,
                (MapCoord.Y - MapAnchor.Y) / MapScale + tab3DCanvas.ActualHeight / 2)
                );
            trGroup.Children.Add(new ScaleTransform(MapScale, MapScale, 1280 + tab3DCanvas.ActualWidth / 2, 1280 + tab3DCanvas.ActualHeight / 2));
            tab2DCanvas.RenderTransform = trGroup;

            //Debug.WriteLine("MapCoord : {0}, {1}", MapCoord.X, MapCoord.Y);
        }

        #endregion 2DWorldTransform

        private void ViewportView_KeyDown(object sender, KeyEventArgs e)
        {
            Debug.WriteLine("Key Viewport Focus : " + e.Key);

            switch (e.Key)
            {
                case Key.N:
                    e.Handled = true;
                    break;
            }
        }

        private void ViewSelectionBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            e.Handled = true;
            if (e.AddedItems.Count < 1)
            {
                UpdateViewportViewsComboBox();
                return;
            }
            string item = (string)e.AddedItems[0];
            SetNewViewType(item);
        }

        private void SetNewViewType(string item)
        {
            ViewportViews newView;
            switch (item)
            {
                case "Top":
                    newView = ViewportViews.Top;
                    break;
                case "Bottom":
                    newView = ViewportViews.Bottom;
                    break;
                case "Front":
                    newView = ViewportViews.Front;
                    break;
                case "Back":
                    newView = ViewportViews.Back;
                    break;
                case "Left":
                    newView = ViewportViews.Left;
                    break;
                case "Right":
                    newView = ViewportViews.Right;
                    break;
                default:
                    newView = ViewportViews.ThreeD;
                    break;
            }
            if (newView == CurrentView)
                return;

            if (newView == ViewportViews.ThreeD)
            {
                ScaleSelectionBox.IsEnabled = false;

                //Clear2DPath();
                //Clear3DPath();
                //Create3DPath();
                //_mvTrack.Content = _mgTrack;
                //_mvPath.Content = _mgPath;
                vp3D.Visibility = Visibility.Visible;
                tab2DCanvas.Visibility = Visibility.Hidden;
                apply3DTransform();
            }
            else
            {
                ScaleSelectionBox.IsEnabled = true;
                
                //Clear3DPath();
                //Clear2DPath();
                //Create2DPath();
                //_mvTrack.Content = null;
                //_mvPath.Content = null;

                // Decide what background to draw
                SetTrackBackground(newView);

                // View refreshing
                if (CurrentView == ViewportViews.ThreeD)
                {
                    MapCoord.X = -WorldCam.PosX * MapScale;
                    MapCoord.Y = -WorldCam.PosY * MapScale;
                }
                apply2DTransform();

                // Make visible
                tab2DCanvas.Visibility = Visibility.Visible;
                vp3D.Visibility = Visibility.Hidden;
            }

            CurrentView = newView;

            Debug.WriteLine("Switched view : " + CurrentView);
            LfsRecordController.ViewportView = CurrentView;
            ServicesFactory.EventService.GetEvent<ViewportViewChangedEvent>().Publish(new ViewportViewChangedEventArgs() { Sender = this, View = CurrentView});
        }

        private void Viewport_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (CurrentView != ViewportViews.ThreeD)
                apply2DTransform();
        }
    }

    struct OrigCamState
    {
        public Point3D Position { get; set; }
        public Point3D LookAt { get; set; }
        public double Heading { get; set; }
        public double Pitch { get; set; }
    }
}
